import * as os from 'os';
import * as path from 'path';

import { ResolvedForgeConfig } from '@electron-forge/shared-types';
import { IgnoreFunction } from '@electron/packager';
import { expect } from 'chai';
import * as fs from 'fs-extra';

import { WebpackPluginConfig, WebpackPluginRendererConfig } from '../src/Config';
import { WebpackPlugin } from '../src/WebpackPlugin';

describe('WebpackPlugin', () => {
  const baseConfig: WebpackPluginConfig = {
    mainConfig: {},
    renderer: {
      config: {},
      entryPoints: [],
    },
  };

  const webpackTestDir = path.resolve(os.tmpdir(), 'electron-forge-plugin-webpack-test');

  describe('TCP port', () => {
    it('should fail for privileged ports', () => {
      expect(() => new WebpackPlugin({ ...baseConfig, loggerPort: 80 })).to.throw(/privileged$/);
    });

    it('should fail for too-large port numbers', () => {
      expect(() => new WebpackPlugin({ ...baseConfig, loggerPort: 99999 })).to.throw(/not a valid TCP port/);
    });
  });

  describe('packageAfterCopy', () => {
    const packageJSONPath = path.join(webpackTestDir, 'package.json');
    const packagedPath = path.join(webpackTestDir, 'packaged');
    const packagedPackageJSONPath = path.join(packagedPath, 'package.json');
    let plugin: WebpackPlugin;

    before(async () => {
      await fs.ensureDir(packagedPath);
      plugin = new WebpackPlugin(baseConfig);
      plugin.setDirectories(webpackTestDir);
    });

    it('should remove config.forge from package.json', async () => {
      const packageJSON = { main: './.webpack/main', config: { forge: 'config.js' } };
      await fs.writeJson(packageJSONPath, packageJSON);
      await plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath);
      expect(await fs.pathExists(packagedPackageJSONPath)).to.equal(true);
      expect((await fs.readJson(packagedPackageJSONPath)).config).to.not.have.property('forge');
    });

    it('should succeed if there is no config.forge', async () => {
      const packageJSON = { main: '.webpack/main' };
      await fs.writeJson(packageJSONPath, packageJSON);
      await plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath);
      expect(await fs.pathExists(packagedPackageJSONPath)).to.equal(true);
      expect(await fs.readJson(packagedPackageJSONPath)).to.not.have.property('config');
    });

    it('should fail if there is no main key in package.json', async () => {
      const packageJSON = {};
      await fs.writeJson(packageJSONPath, packageJSON);
      await expect(plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath)).to.eventually.be.rejectedWith(/entry point/);
    });

    it('should fail if main in package.json does not end with .webpack/main', async () => {
      const packageJSON = { main: 'src/main.js' };
      await fs.writeJson(packageJSONPath, packageJSON);
      await expect(plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath)).to.eventually.be.rejectedWith(/entry point/);
    });

    after(async () => {
      await fs.remove(webpackTestDir);
    });
  });

  describe('resolveForgeConfig', () => {
    let plugin: WebpackPlugin;

    before(() => {
      plugin = new WebpackPlugin(baseConfig);
    });

    it('sets packagerConfig and packagerConfig.ignore if it does not exist', async () => {
      const config = await plugin.resolveForgeConfig({} as ResolvedForgeConfig);
      expect(config.packagerConfig).to.not.equal(undefined);
      expect(config.packagerConfig.ignore).to.be.a('function');
    });

    describe('packagerConfig.ignore', () => {
      it('does not overwrite an existing ignore value', async () => {
        const config = await plugin.resolveForgeConfig({
          packagerConfig: {
            ignore: /test/,
          },
        } as ResolvedForgeConfig);

        expect(config.packagerConfig.ignore).to.deep.equal(/test/);
      });

      it('ignores everything but files in .webpack', async () => {
        const config = await plugin.resolveForgeConfig({} as ResolvedForgeConfig);
        const ignore = config.packagerConfig.ignore as IgnoreFunction;

        expect(ignore('')).to.equal(false);
        expect(ignore('/abc')).to.equal(true);
        expect(ignore('/.webpack')).to.equal(false);
        expect(ignore('/.webpack/foo')).to.equal(false);
      });

      it('ignores files generated by jsonStats config', async () => {
        const webpackConfig = { ...baseConfig, jsonStats: true };
        (webpackConfig.renderer as WebpackPluginRendererConfig).jsonStats = true;
        plugin = new WebpackPlugin(webpackConfig);
        const config = await plugin.resolveForgeConfig({} as ResolvedForgeConfig);
        const ignore = config.packagerConfig.ignore as IgnoreFunction;

        expect(ignore(path.join('foo', 'bar', '.webpack', 'main', 'stats.json'))).to.equal(true);
        expect(ignore(path.join('foo', 'bar', '.webpack', 'renderer', 'stats.json'))).to.equal(true);
      });

      it('ignores source map files by default', async () => {
        const webpackConfig = { ...baseConfig };
        plugin = new WebpackPlugin(webpackConfig);
        const config = await plugin.resolveForgeConfig({} as ResolvedForgeConfig);
        const ignore = config.packagerConfig.ignore as IgnoreFunction;

        expect(ignore(path.join('/.webpack', 'main', 'index.js'))).to.equal(false);
        expect(ignore(path.join('/.webpack', 'main', 'index.js.map'))).to.equal(true);
        expect(ignore(path.join('/.webpack', 'renderer', 'main_window', 'index.js'))).to.equal(false);
        expect(ignore(path.join('/.webpack', 'renderer', 'main_window', 'index.js.map'))).to.equal(true);
      });

      it('includes source map files when specified by config', async () => {
        const webpackConfig = { ...baseConfig, packageSourceMaps: true };
        plugin = new WebpackPlugin(webpackConfig);
        const config = await plugin.resolveForgeConfig({} as ResolvedForgeConfig);
        const ignore = config.packagerConfig.ignore as IgnoreFunction;

        expect(ignore(path.join('/.webpack', 'main', 'index.js'))).to.equal(false);
        expect(ignore(path.join('/.webpack', 'main', 'index.js.map'))).to.equal(false);
        expect(ignore(path.join('/.webpack', 'renderer', 'main_window', 'index.js'))).to.equal(false);
        expect(ignore(path.join('/.webpack', 'renderer', 'main_window', 'index.js.map'))).to.equal(false);
      });
    });
  });

  describe('devServerOptions', () => {
    it('can override defaults', () => {
      const defaultPlugin = new WebpackPlugin(baseConfig);
      defaultPlugin.setDirectories(webpackTestDir);
      expect(defaultPlugin.devServerOptions().hot).to.equal(true);

      const webpackConfig = {
        ...baseConfig,
        devServer: {
          hot: false,
        },
      };

      const plugin = new WebpackPlugin(webpackConfig);
      plugin.setDirectories(webpackTestDir);
      const devServerConfig = plugin.devServerOptions();

      expect(devServerConfig.hot).to.equal(false);
    });

    it('cannot override certain configuration', () => {
      const webpackConfig = {
        ...baseConfig,
        port: 9999,
        devServer: {
          port: 8888,
          headers: {
            'Content-Security-Policy': 'invalid',
            'X-Test-Header': 'test',
          },
        },
      };

      const plugin = new WebpackPlugin(webpackConfig);
      plugin.setDirectories(webpackTestDir);
      const devServerConfig = plugin.devServerOptions();

      expect(devServerConfig.port).to.equal(9999);
      const headers = devServerConfig.headers as Record<string, string>;
      expect(headers['Content-Security-Policy']).to.not.equal('invalid');
      expect(headers['X-Test-Header']).to.equal('test');
    });
  });
});
